<?php
/**
 * Nuwani-v2 Bot Framework
 *
 * This file is part of the Nuwani v2 Bot Framework, a simple set of PHP classes which allow
 * you to set-up and run your own bot. It features advanced, PHP-5.3 based syntax for
 * optimal performance and security.
 *
 * Author: Peter Beverloo
 *         peter@lvp-media.com
 */

class Evaluation extends ModuleBase implements ISecurityProvider
{
	/**
	 * Property: m_aOwnerInfo
	 *
	 * The property which contains an array of the people who will be
	 * allowed to evaluate PHP code.
	 */
	
	private $m_aOwnerInfo;
	
	/**
	 * Property: m_sPrefix
	 *
	 * Determaines the prefix of the evaluation messages, which can be
	 * a single character, but also a line of text.
	 */
	
	private $m_sPrefix;
	
	/**
	 * Function: __construct
	 *
	 * The constructor will initialise a list of the people who are allowed
	 * to evaluate direct PHP code, using the configuration manager.
	 */
	
	public function __construct ()
	{
		$pConfiguration = Configuration :: getInstance ();
		$aConfiguration = $pConfiguration -> get ('Owners');
		
		if (!count ($aConfiguration))
			return ;
		
		$this -> m_sPrefix = $aConfiguration ['Prefix'];
		$this -> m_aOwnerInfo = array ();
		
		for ($i = 0; isset ($aConfiguration [$i]); $i++)
		{
			list ($Nickname, $Username, $Hostname) = preg_split ('/!|@/s', $aConfiguration [$i] ['Username']);
			$this -> m_aOwnerInfo [] = array
			(
				'Nickname'	=> $Nickname,
				'Username'	=> $Username,
				'Hostname'	=> $Hostname,
				'Password'	=> $aConfiguration [$i] ['Password'],
				'Identified'	=> (isset ($aConfiguration [$i] ['Identified']) ? $aConfiguration [$i] ['Identified'] : false),
				'CachedUser'	=> ''
			);
		}
		
		// And notofy the console that we've finished loading
		echo 'Loaded the Evaluation module..' . NL;
	}
	
	/**
	 * Function: onModuleLoad
	 * Argument: pModule (class) - The module that is being loaded
	 *
	 * This function will be called whenever a new module is being loaded. In here
	 * we simply check whether we have to re-register ourselfes as a security module.
	 */
	
	public function onModuleLoad ($pModule)
	{
		if (in_array ('ISecurityModule', class_implements ($pModule)))
			$pModule -> registerSecurityProvider ($this, ISecurityProvider :: BOT_OWNER);
	}
	
	/**
	 * Function: checkSecurity
	 * Argument: pBot (Bot) - The bot which we should check security against.
	 * Argument: nSecurityLevel (integer) - Related level of security.
	 *
	 * This function will check whether the security level as specified by the
	 * second argument against the user the $pBot variable contains.
	 */
	
	public function checkSecurity (Bot $pBot, $nSecurityLevel)
	{
		$sUser = $pBot -> In -> User;
		foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
		{
			if ($aOwnerInfo ['Identified'] === true && 
			   ($aOwnerInfo ['CachedUser'] == $sUser || 
			   ($aOwnerInfo ['CachedUser'] == '' && $this -> confirmIdentity ($pBot, $aOwnerInfo))))
			{
				return true ;
			}
		}
		
		return false ;
	}
	
	/**
	 * Function: onChangeNick
	 * Argument: pBot (Bot) - The bot which received this message.
	 * Argument: sOldName (string) - Old nickname of the person who changes his nick
	 * Argument: sNewName (string) - And the new nickname of this same person.
	 *
	 * This function gets invoked when someone changes their nickname. In here we
	 * have to check whether this person was logged in with this module.
	 */
	
	public function onChangeNick (Bot $pBot, $sOldName, $sNewName)
	{
		$sUser = $pBot -> In -> User;
		foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
		{
			if ($aOwnerInfo ['Identified'] === true && $aOwnerInfo ['CachedUser'] == $sUser)
			{
				$aOwnerInfo ['CachedUser'] = $sNewName . '!' . $pBot -> In -> Username . '@' . $pBot -> In -> Hostname;
				return ;
			}
		}
	}
	
	/**
	 * Function: onQuit
	 * Argument: pBot (Bot) - The bot which received this message.
	 * Argument: sNickname (string) - The user who is quitting IRC.
	 * Argument: sReason (string) - Why is this user quitting IRC?
	 *
	 * When one of the owners quits IRC, we log them out for security purposes.
	 * Some random guy might take his/her nickname and immitate the user!
	 */
	
	public function onQuit (Bot $pBot, $sNickname, $sReason)
	{
		$sUser = $pBot -> In -> User;
		foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
		{
			if ($aOwnerInfo ['Identified'] === true && $aOwnerInfo ['CachedUser'] == $sUser)
			{
				$aOwnerInfo ['CachedUser'] = '';
				$aOwnerInfo ['Identified'] = false;
				return ;
			}
		}
	}
	
	/**
	 * Function: onChannelPrivmsg
	 * Argument: pBot (Bot) - The bot who received the public channel message
	 * Argument: sChannel (string) - Channel in which we received the message
	 * Argument: sNickname (string) - The nickname associated with this message
	 * Argument: sMessage (string) - And of course the actual message we received
	 *
	 * PHP Code evaluations may occur in public channels, such as #Sonium which
	 * is frequently used for testing. This function will check whether we're
	 * allowed to evaluate anything here.
	 */
	
	public function onChannelPrivmsg (Bot $pBot, $sChannel, $sNickname, $sMessage)
	{
		if (substr ($sMessage, 0, strlen ($this -> m_sPrefix)) != $this -> m_sPrefix)
			return false ;
		
		$sUser = $pBot -> In -> User;
		foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
		{
			if ($aOwnerInfo ['Identified'] === true && ($aOwnerInfo ['CachedUser'] == $sUser || ($aOwnerInfo ['CachedUser'] == '' && $this -> confirmIdentity ($pBot, $aOwnerInfo))))
			{
				if ($aOwnerInfo ['CachedUser'] == '')
					$aOwnerInfo ['CachedUser'] = $sUser;
				
				$this -> parseEvaluation ($pBot, $sChannel, $sMessage);
				return true ;
			}
		}
		
		return false ;
	}
	
	/**
	 * Function: onPrivmsg
	 * Argument: pBot (string) - Bot that received this privage message
	 * Argument: sNickname (string) - Source of the message
	 * Argument: sMessage (string) - The message that got PM'ed to us.
	 *
	 * People are free to send private messages to the bot, which gets
	 * handled right here. This function does the same as onChannelPrivmsg
	 */
	
	public function onPrivmsg (Bot $pBot, $sNickname, $sMessage)
	{
		if (substr ($sMessage, 0, strlen ($this -> m_sPrefix)) != $this -> m_sPrefix)
		{
			if (substr ($sMessage, 0, 6) == 'login ')
			{
				foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
				{
					if (($aOwnerInfo ['Nickname'] == $pBot -> In -> Nickname || $aOwnerInfo ['Nickname'] == '*') &&
					    ($aOwnerInfo ['Username'] == $pBot -> In -> Username || $aOwnerInfo ['Username'] == '*') &&
					    ($aOwnerInfo ['Hostname'] == $pBot -> In -> Hostname || $aOwnerInfo ['Hostname'] == '*'))
					{
						if (md5 (substr ($sMessage, 6)) == $aOwnerInfo ['Password']) {
							$aOwnerInfo ['CachedUser'] = $pBot -> In -> User;
							$aOwnerInfo ['Identified'] = true ;
						
							$pBot -> send ('PRIVMSG ' . $sNickname . ' :You have successfully identified yourself!');
							return true ;
						} else {
							$pBot -> send ('PRIVMSG ' . $sNickname . ' :Incorrect password.');
							return false ;
						}
					}
				}
				
				return false ;
			}
			
			if (substr ($sMessage, 0, 6) == 'logout')
			{
				$sUser = $pBot -> In -> User;
				foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
				{
					if ($aOwnerInfo ['Identified'] === true && $aOwnerInfo ['CachedUser'] == $sUser)
					{
						$aOwnerInfo ['Identified'] = false ;
						$aOwnerInfo ['CachedUser'] = '';
						
						$pBot -> send ('PRIVMSG ' . $sNickname . ' :You have been logged-out successfully.');
						return true ;
					}
				}
			}
			
			return false ;
		}
		
		$sUser = $pBot -> In -> User;
		foreach ($this -> m_aOwnerInfo as & $aOwnerInfo)
		{
			if ($aOwnerInfo ['Identified'] === true && ($aOwnerInfo ['CachedUser'] == $sUser || ($aOwnerInfo ['CachedUser'] == '' && $this -> confirmIdentity ($pBot, $aOwnerInfo))))
			{
				if ($aOwnerInfo ['CachedUser'] == '')
					$aOwnerInfo ['CachedUser'] = $sUser;
				
				$this -> parseEvaluation ($pBot, $sNickname, $sMessage);
				return true ;
			}
		}
		
		return false ;
	}
	
	/**
	 * Function: parseEvaluation
	 * Argument: pBot (Bot) - Bot that should handle the evaluation
	 * Argument: sDestination (string) - Where should the output be redirected?
	 * Argument: sEvaluation (string) - Line of (PHP?) code that should be executed
	 *
	 * After we have confirmed the identity of the one evaluating code on
	 * this bot, we're ready to parse the evaluation string into the options.
	 */
	
	private function parseEvaluation (Bot $pBot, $sDestination, $sEvaluation)
	{
		$sEvaluation = substr ($sEvaluation, strlen ($this -> m_sPrefix));
		$aOptions = array
		(
			'Type'		=> 'PHP',
			'Buffering'	=> true,
			'Operation'	=> substr ($sEvaluation, strpos ($sEvaluation, ' ') + 1),
			'Delay'		=> 0
		);
		
		if (substr ($sEvaluation, 0, 1) != ' ') // Parse extra options
		{
			$sEvaluationIdent = substr ($sEvaluation, 0, strpos ($sEvaluation, ' '));
			if (strpos ($sEvaluationIdent, '!') !== false)
			{
				$sEvaluationIdent = str_replace ('!', '', $sEvaluationIdent);
				$aOptions ['Type'] = 'EXEC';
			}
			
			$aMatches = array ();
			preg_match_all ('/(\d+)(s|m|h)/', $sEvaluationIdent, $aMatches);
			foreach ($aMatches [0] as $iIndex => $sMatch)
			{
				$nThisDelay = $aMatches [1] [$iIndex];
				switch ($aMatches [2] [$iIndex])
				{
					case 'h':
					case 'H':
						$nThisDelay *= 60; // drop through
						
					case 'm':
					case 'M':
						$nThisDelay *= 60;
						break;
				}
				
				$sEvaluationIdent = str_replace ($sMatch, '', $sEvaluationIdent);
				$aOptions ['Delay'] += $nThisDelay;
			}
			
			if ($sEvaluationIdent == 'out')
				$aOptions ['Buffering'] = false ;
		}
		
		if ($aOptions ['Delay'] > 0)
		{
			Timer::Create (function () use ($pBot, $sDestination, $aOptions) 
				{
					$pModules = ModuleManager :: getInstance ();
					if (isset ($pModules ['Evaluation']))
					{
						$pModules ['Evaluation'] -> doEvaluation ($pBot, $sDestination, $aOptions);
					}
				}, $aOptions ['Delay'] * 1000, false);
			
			return true ;
		}
		
		$this -> doEvaluation ($pBot, $sDestination, $aOptions);
		return true ;
	}
	
	/**
	 * Function: doEvaluation
	 * Argument: pBot (Bot) - Bot that should handle the evaluation
	 * Argument: sDestination (string) - Where should the output be redirected?
	 * Argument: aOptions (array) - Array with the parsed options for this evalaution.
	 *
	 * This function will immediatly execute the evaluation that we're doing right
	 * now. Evaluation delays are already handled by the parser.
	 */
	
	public function doEvaluation (Bot $pBot, $sDestination, $aOptions)
	{
		ob_start ();
		ErrorExceptionHandler :: $Source = $sDestination ;
		
		switch ($aOptions ['Type'])
		{
			case 'PHP':
				eval ($aOptions ['Operation']);
				break;
			
			case 'EXEC':
				echo shell_exec ($aOptions ['Operation']);
				break;
		}
		
		$aOutput = explode (NL, trim (ob_get_clean ()));
		if (count ($aOutput) > 4 && $aOptions ['Buffering'] == true)
		{
			$nSize   = count ($aOutput);
			$aOutput = array_slice ($aOutput, 0, 4);
			$aOutput [] = '10Displayed 4 out of ' . $nSize . ' lines..';
		}
		
		if (isset ($pBot) && $pBot instanceof Bot)
		{
			foreach ($aOutput as $sLine)
			{
				$pBot -> send ('PRIVMSG ' . $sDestination . ' :' . trim ($sLine), false);
			}
		}
	}

	/**
	 * Function: confirmIdentity
	 * Argument: pBot (Bot) - Bot that this message is incoming on.
	 * Argument: aOwnerInfo (array) - Information about the owner to check
	 * 
	 * This function will try to confirm the identity of this logged in owner, who
	 * has no hostname cached. Could in theory be a performance penalty.
	 */
	
	private function confirmIdentity (Bot $pBot, & $aOwnerInfo)
	{
		if (($aOwnerInfo ['Nickname'] == $pBot -> In -> Nickname || $aOwnerInfo ['Nickname'] == '*') &&
		    ($aOwnerInfo ['Username'] == $pBot -> In -> Username || $aOwnerInfo ['Username'] == '*') &&
		    ($aOwnerInfo ['Hostname'] == $pBot -> In -> Hostname || $aOwnerInfo ['Hostname'] == '*'))
		{
			$aOwnerInfo ['CachedUser'] = $pBot -> In -> User;
			return true ;
		}
		
		return false ;
	}
};

?>